JavaScript olay döngüsü, görev kuyrukları ve mikro görev kuyruklarının derinlemesine incelenmesi. JavaScript'in tek iş parçacıklı ortamlarda nasıl eş zamanlılık sağladığını açıklar.
JavaScript Olay Döngüsünü Anlamak: Görev Kuyrukları ve Mikro Görev Yönetimi
JavaScript, tek iş parçacıklı bir dil olmasına rağmen, eş zamanlılığı ve asenkron işlemleri verimli bir şekilde yönetmeyi başarır. Bu, dahiyane Olay Döngüsü sayesinde mümkün olur. Performanslı ve duyarlı uygulamalar yazmayı amaçlayan herhangi bir JavaScript geliştiricisi için bunun nasıl çalıştığını anlamak çok önemlidir. Bu kapsamlı kılavuz, Olay Döngüsü'nün karmaşıklıklarını keşfedecek ve Görev Kuyruğu (Geri Çağırma Kuyruğu olarak da bilinir) ve Mikro Görev Kuyruğu'na odaklanacaktır.
JavaScript Olay Döngüsü Nedir?
Olay Döngüsü, çağrı yığınını ve görev kuyruğunu izleyen sürekli çalışan bir işlemdir. Birincil işlevi, çağrı yığınının boş olup olmadığını kontrol etmektir. Eğer boşsa, Olay Döngüsü görev kuyruğundan ilk görevi alır ve yürütme için çağrı yığınına iter. Bu işlem süresiz olarak tekrarlanır ve JavaScript'in birden çok işlemi görünüşte eş zamanlı olarak işlemesine olanak tanır.
Bunu sürekli olarak iki şeyi kontrol eden çalışkan bir işçi olarak düşünün: "Şu anda bir şey üzerinde çalışıyor muyum (çağrı yığını)?" ve "Yapmamı bekleyen bir şey var mı (görev kuyruğu)?" İşçi boşta ise (çağrı yığını boş) ve bekleyen görevler varsa (görev kuyruğu boş değil), işçi sonraki görevi alır ve üzerinde çalışmaya başlar.
Esasen, Olay Döngüsü, JavaScript'in engellemeyen işlemler gerçekleştirmesini sağlayan motordur. O olmadan, JavaScript yalnızca sıralı olarak kod yürütmekle sınırlı kalır ve özellikle web tarayıcılarında ve G/Ç işlemleri, kullanıcı etkileşimleri ve diğer asenkron olaylarla ilgilenen Node.js ortamlarında kötü bir kullanıcı deneyimine yol açar.
Çağrı Yığını: Kodun Yürütüldüğü Yer
Çağrı Yığını, Son Giren İlk Çıkar (LIFO) ilkesini izleyen bir veri yapısıdır. JavaScript kodunun gerçekten yürütüldüğü yerdir. Bir fonksiyon çağrıldığında, Çağrı Yığını'na itilir. Fonksiyon yürütmesini tamamladığında, yığından çıkarılır.
Bu basit örneği ele alalım:
function firstFunction() {
console.log('First function');
secondFunction();
}
function secondFunction() {
console.log('Second function');
}
firstFunction();
İşte yürütme sırasında Çağrı Yığını'nın nasıl görüneceği:
- Başlangıçta, Çağrı Yığını boştur.
firstFunction()çağrılır ve yığına itilir.firstFunction()içinde,console.log('First function')yürütülür.secondFunction()çağrılır ve yığına itilir (firstFunction()'ın üstüne).secondFunction()içinde,console.log('Second function')yürütülür.secondFunction()tamamlanır ve yığından çıkarılır.firstFunction()tamamlanır ve yığından çıkarılır.- Çağrı Yığını şimdi tekrar boş.
Bir fonksiyon, uygun bir çıkış koşulu olmadan kendisini yinelemeli olarak çağırırsa, Çağrı Yığını'nın maksimum boyutunu aştığı ve programın çökmesine neden olduğu bir Yığın Taşması hatasına yol açabilir.
Görev Kuyruğu (Geri Çağırma Kuyruğu): Asenkron İşlemleri İşleme
Görev Kuyruğu (Geri Çağırma Kuyruğu veya Makro Görev Kuyruğu olarak da bilinir), Olay Döngüsü tarafından işlenmeyi bekleyen görevlerin bir kuyruğudur. Aşağıdaki gibi asenkron işlemleri işlemek için kullanılır:
setTimeoutvesetIntervalgeri çağırmaları- Olay dinleyicileri (örneğin, tıklama olayları, tuşa basma olayları)
XMLHttpRequest(XHR) vefetchgeri çağırmaları (ağ istekleri için)- Kullanıcı etkileşimi olayları
Bir asenkron işlem tamamlandığında, geri çağırma fonksiyonu Görev Kuyruğu'na yerleştirilir. Olay Döngüsü daha sonra bu geri çağırmaları tek tek alır ve çağrı yığını boş olduğunda bunları Çağrı Yığını'nda yürütür.
Bunu bir setTimeout örneğiyle gösterelim:
console.log('Başla');
setTimeout(() => {
console.log('Zaman aşımı geri çağırması');
}, 0);
console.log('Bitir');
Çıktının şu olmasını bekleyebilirsiniz:
Başla
Zaman aşımı geri çağırması
Bitir
Ancak, gerçek çıktı şöyledir:
Başla
Bitir
Zaman aşımı geri çağırması
İşte nedeni:
console.log('Başla')yürütülür ve "Başla" yazdırılır.setTimeout(() => { ... }, 0)çağrılır. Gecikme 0 milisaniye olmasına rağmen, geri çağırma fonksiyonu hemen yürütülmez. Bunun yerine, Görev Kuyruğu'na yerleştirilir.console.log('Bitir')yürütülür ve "Bitir" yazdırılır.- Çağrı Yığını şimdi boş. Olay Döngüsü Görev Kuyruğu'nu kontrol eder.
setTimeout'tan gelen geri çağırma fonksiyonu Görev Kuyruğu'ndan Çağrı Yığını'na taşınır ve yürütülerek "Zaman aşımı geri çağırması" yazdırılır.
Bu, 0 ms'lik bir gecikmeyle bile, setTimeout geri çağırmalarının her zaman asenkron olarak, mevcut senkron kod çalışmayı bitirdikten sonra yürütüldüğünü gösterir.
Mikro Görev Kuyruğu: Görev Kuyruğundan Daha Yüksek Öncelikli
Mikro Görev Kuyruğu, Olay Döngüsü tarafından yönetilen başka bir kuyruktur. Mevcut görev tamamlandıktan sonra mümkün olan en kısa sürede, ancak Olay Döngüsü yeniden oluşturmadan veya diğer olayları işlemeden önce yürütülmesi gereken görevler için tasarlanmıştır. Bunu, Görev Kuyruğu'na kıyasla daha yüksek öncelikli bir kuyruk olarak düşünün.
Mikro görevlerin yaygın kaynakları şunlardır:
- Promises: Promises'ın
.then(),.catch()ve.finally()geri çağırmaları Mikro Görev Kuyruğu'na eklenir. - MutationObserver: DOM'daki (Belge Nesne Modeli) değişiklikleri gözlemlemek için kullanılır. Mutasyon gözlemcisi geri çağırmaları da Mikro Görev Kuyruğu'na eklenir.
process.nextTick()(Node.js): Geçerli işlem tamamlandıktan sonra, ancak Olay Döngüsü devam etmeden önce yürütülecek bir geri çağırmayı planlar. Güçlü olmasına rağmen, aşırı kullanımı G/Ç yetersizliğine yol açabilir.queueMicrotask()(Nispeten yeni tarayıcı API'sı): Bir mikro görevi sıraya koymanın standartlaştırılmış bir yolu.
Görev Kuyruğu ve Mikro Görev Kuyruğu arasındaki temel fark, Olay Döngüsü'nün Görev Kuyruğu'ndan bir sonraki görevi almadan önce Mikro Görev Kuyruğu'ndaki tüm kullanılabilir mikro görevleri işlemesidir. Bu, mikro görevlerin her görev tamamlandıktan sonra derhal yürütülmesini, potansiyel gecikmeleri en aza indirmeyi ve yanıt verme hızını iyileştirmeyi sağlar.
Promises ve setTimeout içeren bu örneği ele alalım:
console.log('Başla');
Promise.resolve().then(() => {
console.log('Promise geri çağırması');
});
setTimeout(() => {
console.log('Zaman aşımı geri çağırması');
}, 0);
console.log('Bitir');
Çıktı şu olacaktır:
Başla
Bitir
Promise geri çağırması
Zaman aşımı geri çağırması
İşte dökümü:
console.log('Başla')yürütülür.Promise.resolve().then(() => { ... })çözümlenmiş bir Promise oluşturur..then()geri çağırması Mikro Görev Kuyruğu'na eklenir.setTimeout(() => { ... }, 0)geri çağırmasını Görev Kuyruğu'na ekler.console.log('Bitir')yürütülür.- Çağrı Yığını boştur. Olay Döngüsü önce Mikro Görev Kuyruğu'nu kontrol eder.
- Promise geri çağırması Mikro Görev Kuyruğu'ndan Çağrı Yığını'na taşınır ve yürütülerek "Promise geri çağırması" yazdırılır.
- Mikro Görev Kuyruğu şimdi boş. Olay Döngüsü daha sonra Görev Kuyruğu'nu kontrol eder.
setTimeoutgeri çağırması Görev Kuyruğu'ndan Çağrı Yığını'na taşınır ve yürütülerek "Zaman aşımı geri çağırması" yazdırılır.
Bu örnek, mikro görevlerin (Promise geri çağırmaları) görevlerden (setTimeout geri çağırmaları) önce, setTimeout gecikmesi 0 olduğunda bile yürütüldüğünü açıkça gösterir.
Önceliğin Önemi: Mikro Görevler ve Görevler
Mikro görevlerin görevlere göre önceliklendirilmesi, duyarlı bir kullanıcı arayüzü sağlamak için çok önemlidir. Mikro görevler genellikle DOM'u güncellemek veya kritik veri değişikliklerini işlemek için mümkün olan en kısa sürede yürütülmesi gereken işlemleri içerir. Mikro görevleri görevlerden önce işleyerek, tarayıcı bu güncellemelerin hızla yansıtılmasını sağlayabilir, uygulamanın algılanan performansını iyileştirebilir.
Örneğin, bir sunucudan alınan verilere göre kullanıcı arayüzünü güncellediğiniz bir durumu hayal edin. Veri işlemeyi ve kullanıcı arayüzü güncellemelerini işlemek için Promises (Mikro Görev Kuyruğu'nu kullanan) kullanmak, değişikliklerin hızla uygulanmasını sağlayarak daha sorunsuz bir kullanıcı deneyimi sağlar. Bu güncellemeler için setTimeout (Görev Kuyruğu'nu kullanan) kullanırsanız, fark edilebilir bir gecikme olabilir ve bu da daha az duyarlı bir uygulamaya yol açar.
Yetersizlik: Mikro Görevler Olay Döngüsünü Engellediğinde
Mikro Görev Kuyruğu yanıt verme hızını iyileştirmek için tasarlanmış olsa da, onu dikkatli kullanmak çok önemlidir. Olay Döngüsü'nün Görev Kuyruğu'na geçmesine veya güncellemeleri işlemesine izin vermeden sürekli olarak kuyruğa mikro görevler eklerseniz, yetersizliğe neden olabilirsiniz. Bu, Mikro Görev Kuyruğu asla boşalmadığında, Olay Döngüsü'nü etkili bir şekilde bloke ederek diğer görevlerin yürütülmesini engellediğinde meydana gelir.
Bu örneği ele alalım (öncelikle process.nextTick'in mevcut olduğu Node.js gibi ortamlarda geçerlidir, ancak kavramsal olarak başka yerlerde de uygulanabilir):
function starve() {
Promise.resolve().then(() => {
console.log('Mikro görev yürütüldü');
starve(); // Yinelemeli olarak başka bir mikro görev ekle
});
}
starve();
Bu örnekte, starve() fonksiyonu sürekli olarak Mikro Görev Kuyruğu'na yeni Promise geri çağırmaları ekler. Olay Döngüsü, bu mikro görevleri süresiz olarak işlemeye takılır ve diğer görevlerin yürütülmesini engeller ve potansiyel olarak donmuş bir uygulamaya yol açar.
Yetersizliği Önlemek İçin En İyi Uygulamalar:
- Tek bir görev içinde oluşturulan mikro görevlerin sayısını sınırlayın. Olay Döngüsü'nü engelleyebilecek yinelemeli mikro görev döngüleri oluşturmaktan kaçının.
- Daha az kritik işlemler için
setTimeoutkullanmayı düşünün. Bir işlemin hemen yürütülmesi gerekmiyorsa, onu Görev Kuyruğu'na ertelemek Mikro Görev Kuyruğu'nun aşırı yüklenmesini önleyebilir. - Mikro görevlerin performans etkilerinin farkında olun. Mikro görevler genellikle görevlerden daha hızlı olsa da, aşırı kullanım yine de uygulama performansını etkileyebilir.
Gerçek Dünya Örnekleri ve Kullanım Durumları
Örnek 1: Promises ile Asenkron Görüntü Yükleme
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Görüntü yüklenemedi: ${url}`));
img.src = url;
});
}
// Örnek kullanım:
loadImage('https://example.com/image.jpg')
.then(img => {
// Görüntü başarıyla yüklendi. DOM'u güncelle.
document.body.appendChild(img);
})
.catch(error => {
// Görüntü yükleme hatasını işle.
console.error(error);
});
Bu örnekte, loadImage fonksiyonu, görüntü başarıyla yüklendiğinde çözümlenen veya bir hata varsa reddeden bir Promise döndürür. .then() ve .catch() geri çağırmaları, görüntü yükleme işlemi tamamlandıktan sonra DOM güncellemesinin ve hata işlemenin derhal yürütülmesini sağlayarak Mikro Görev Kuyruğu'na eklenir.
Örnek 2: Dinamik Kullanıcı Arayüzü Güncellemeleri için MutationObserver Kullanma
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
console.log('Mutasyon gözlemlendi:', mutation);
// Mutasyona göre kullanıcı arayüzünü güncelle.
});
});
const elementToObserve = document.getElementById('myElement');
observer.observe(elementToObserve, {
attributes: true,
childList: true,
subtree: true
});
// Daha sonra, öğeyi değiştir:
elementToObserve.textContent = 'Yeni içerik!';
MutationObserver, DOM'daki değişiklikleri izlemenizi sağlar. Bir mutasyon meydana geldiğinde (örneğin, bir öznitelik değiştirildiğinde, bir alt düğüm eklendiğinde), MutationObserver geri çağırması Mikro Görev Kuyruğu'na eklenir. Bu, DOM değişikliklerine yanıt olarak kullanıcı arayüzünün hızla güncellenmesini sağlar.
Örnek 3: Fetch API ile Ağ İsteklerini İşleme
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
console.log('Veri alındı:', data);
// Verileri işle ve kullanıcı arayüzünü güncelle.
})
.catch(error => {
console.error('Veri alınırken hata:', error);
// Hatayı işle.
});
Fetch API, JavaScript'te ağ istekleri yapmanın modern bir yoludur. .then() geri çağırmaları, yanıt alındıktan sonra veri işlemenin ve kullanıcı arayüzü güncellemelerinin mümkün olan en kısa sürede yürütülmesini sağlayarak Mikro Görev Kuyruğu'na eklenir.
Node.js Olay Döngüsü Hususları
Node.js'deki Olay Döngüsü, tarayıcı ortamına benzer şekilde çalışır, ancak bazı özel özelliklere sahiptir. Node.js, Olay Döngüsü'nün bir uygulamasını ve asenkron G/Ç yeteneklerini sağlayan libuv kitaplığını kullanır.
process.nextTick(): Daha önce belirtildiği gibi, process.nextTick(), geçerli işlem tamamlandıktan sonra, ancak Olay Döngüsü devam etmeden önce yürütülecek bir geri çağırmayı planlamanıza olanak tanıyan Node.js'ye özgü bir fonksiyondur. process.nextTick() ile eklenen geri çağırmalar, Mikro Görev Kuyruğu'ndaki Promise geri çağırmalarından önce yürütülür. Bununla birlikte, yetersizlik potansiyeli nedeniyle, process.nextTick() dikkatli kullanılmalıdır. queueMicrotask(), kullanılabilir olduğunda genellikle tercih edilir.
setImmediate(): setImmediate() fonksiyonu, Olay Döngüsü'nün sonraki yinelemesinde yürütülecek bir geri çağırmayı planlar. setTimeout(() => { ... }, 0)'a benzer, ancak setImmediate() G/Ç ile ilgili görevler için tasarlanmıştır. setImmediate() ve setTimeout(() => { ... }, 0) arasındaki yürütme sırası tahmin edilemez olabilir ve sistemin G/Ç performansına bağlıdır.
Verimli Olay Döngüsü Yönetimi için En İyi Uygulamalar
- Ana iş parçacığını engellemeyin. Uzun süren senkron işlemler, Olay Döngüsü'nü engelleyerek uygulamanın yanıt vermemesine neden olabilir. Mümkün olduğunda asenkron işlemleri kullanın.
- Kodunuzu optimize edin. Verimli kod daha hızlı yürütülür, Çağrı Yığını'nda harcanan süreyi azaltır ve Olay Döngüsü'nün daha fazla görevi işlemesine olanak tanır.
- Asenkron işlemler için Promises kullanın. Promises, geleneksel geri çağırmalara kıyasla asenkron kodu işlemenin daha temiz ve daha yönetilebilir bir yolunu sağlar.
- Mikro Görev Kuyruğu'nun farkında olun. Yetersizliğe yol açabilecek aşırı mikro görevler oluşturmaktan kaçının.
- Hesaplama açısından yoğun görevler için Web İşçilerini kullanın. Web İşçileri, JavaScript kodunu ayrı iş parçacıklarında çalıştırmanıza olanak tanır ve ana iş parçacığının engellenmesini önler. (Tarayıcı ortamına özgü)
- Kodunuzu profilleyin. Performans darboğazlarını belirlemek ve kodunuzu optimize etmek için tarayıcı geliştirici araçlarını veya Node.js profil oluşturma araçlarını kullanın.
- Olayları geciktirin ve kısın. Sık sık tetiklenen olaylar (örneğin, kaydırma olayları, yeniden boyutlandırma olayları) için, olay işleyicinin yürütülme sayısını sınırlamak için geciktirme veya kısma kullanın. Bu, Olay Döngüsü üzerindeki yükü azaltarak performansı artırabilir.
Sonuç
Performanslı ve duyarlı JavaScript uygulamaları yazmak için JavaScript Olay Döngüsü, Görev Kuyruğu ve Mikro Görev Kuyruğu'nu anlamak çok önemlidir. Olay Döngüsü'nün nasıl çalıştığını anlayarak, asenkron işlemleri nasıl işleyeceğiniz ve daha iyi performans için kodunuzu nasıl optimize edeceğiniz konusunda bilinçli kararlar verebilirsiniz. Mikro görevlere uygun şekilde öncelik vermeyi, yetersizlikten kaçınmayı ve her zaman ana iş parçacığını engelleyici işlemlerden uzak tutmaya çalışmayı unutmayın.
Bu kılavuz, JavaScript Olay Döngüsü'ne kapsamlı bir genel bakış sağlamıştır. Burada özetlenen bilgileri ve en iyi uygulamaları uygulayarak, harika bir kullanıcı deneyimi sunan sağlam ve verimli JavaScript uygulamaları oluşturabilirsiniz.